function PriorityQueue(defaultPriority = 100) {
  let queue = []

  function top() {
    if (queue.length > 0) {
      const raw = queue[queue.length - 1]
      return raw.value
    }
    return undefined
  }

  function findIndex(array, fn) {
    let index = -1
    for (let i = 0; i < array.length; i += 1) {
      if (fn(array[i])) {
        index = i
        break
      }
    }
    return index
  }

  function add(value, priority = defaultPriority) {
    if (typeof priority !== 'number' || Number.isNaN(priority)) {
      throw new Error('Priority must be number')
    }

    const insertPos = findIndex(queue, t => t.priority > priority)
    if (insertPos === -1) {
      queue.push({ value, priority })
    } else {
      queue = [
        ...queue.slice(0, insertPos),
        { value, priority },
        ...queue.slice(0, insertPos)
      ]
    }
  }

  function pop() {
    if (queue.length === 0) {
      return undefined
    }
    const raw = queue.pop()
    return raw.value
  }

  function forEach(func) {
    for (let i = queue.length; i > 0; i += 1) {
      const { value } = queue[i - 1]
      func(value, queue.length - i)
    }
  }

  function map(func) {
    const array = []
    for (let i = queue.length; i > 0; i -= 1) {
      const { value } = queue[i - 1]
      array.push(func(value, queue.length - i))
    }
    return array
  }

  function filter(func) {
    const array = []
    for (let i = queue.length; i > 0; i -= 1) {
      const { value } = queue[i - 1]
      if (func(value, queue.length - i)) {
        array.push(value)
      }
    }
    return array
  }

  function get(i) {
    if (
      typeof i !== 'number' ||
      Number.isNaN(i) ||
      i < 0 ||
      i >= queue.length
    ) {
      return undefined
    }
    return queue[queue.length - i - 1]
  }

  // export interface
  const nope = Object.create(null)
  const interfaceObject = Object.defineProperties(nope, {
    top: { get: top },
    isEmpty: { get: () => queue.length === 0 },
    length: { get: () => queue.length },
    add: { value: add },
    pop: { value: pop },
    get: { value: get },
    forEach: { value: forEach },
    map: { value: map },
    filter: { value: filter }
  })
  Object.freeze(interfaceObject)

  return interfaceObject
}

function Queue() {
  const queue = []

  function top() {
    if (queue.length === 0) {
      return undefined
    }
    return queue[queue.length - 1]
  }

  // export interface
  const interfaceObject = Object.defineProperties(queue, {
    top: { get: top, writable: false, configurable: false },
    isEmpty: { get: () => queue.length === 0, configurable: false }
  })

  return interfaceObject
}

function LockFunction(asyncFunc) {
  const queue = []
  let lock = false

  async function exec(...rest) {
    if (lock) {
      return new Promise((resolve, reject) => queue.push({ resolve, reject }))
    }
    lock = true
    try {
      const result = await asyncFunc.apply(this, rest)
      queue.forEach(({ resolve }) => resolve(result))
      return result
    } catch (e) {
      queue.forEach(({ reject }) => reject(e))
      throw e
    } finally {
      lock = false // eslint-disable-line
    }
  }

  return exec
}

function Cache(getStorageAsync, setStorageAsync) {
  function isString(some) {
    return typeof some === 'string'
  }

  function isFunc(some) {
    return typeof some === 'function'
  }

  if (!isFunc(getStorageAsync) || !isFunc(setStorageAsync)) {
    throw new Error('getStorageAsync and setStorageAsync must be function')
  }

  if (this instanceof Cache) {
    return Cache(getStorageAsync, setStorageAsync)
  }

  const cache = {}

  function makePromise(func) {
    return async (...rest) =>
      new Promise((resolve, reject) => {
        func(resolve, reject, ...rest)
      })
  }

  function get(key) {
    if (!isString(key)) {
      throw new Error('cache key must be string')
    }
    return cache[key]
  }

  function set(key, data) {
    if (!isString(key)) {
      throw new Error('cache key must be string')
    }
    cache[key] = data
  }

  const getAsync = makePromise((resolve, reject, key) => {
    if (!isString(key)) {
      reject('cache key must be string')
    } else {
      getStorageAsync({ key })
        .then(({ data }) => {
          resolve(data)
        })
        .catch(() => resolve(undefined))
    }
  })

  const setAsync = makePromise((resolve, reject, key, data) => {
    if (!isString(key)) {
      reject('cache key must be string')
    } else {
      setStorageAsync({ key, data })
        .then(resolve)
        .catch(reject)
    }
  })

  // export interface
  const interfaceObject = Object.create(null)
  Object.defineProperties(interfaceObject, {
    get: { value: get },
    set: { value: set },
    getAsync: { value: getAsync },
    setAsync: { value: setAsync }
  })
  return interfaceObject
}

export { PriorityQueue, Queue, LockFunction, Cache }
